package generator

import (
	"fmt"
	"v8s/compose/model"

	"v8s-controller/internal"
	"v8s-controller/internal/configuration"

	"k8s.io/apimachinery/pkg/runtime"
)

type (
	Generator struct {
		ResourceQuotaGenerator    *ResourceQuotaGenerator
		LimitRangeGenerator       *LimitRangeGenerator
		VolumeClaimsGenerator     *VolumeClaimsGenerator
		ImagePullSecretsGenerator *ImagePullSecretsGenerator
		DeploymentsGenerator      *DeploymentsGenerator
		ServicesGenerator         *ServiceGenerator
		LoadBalancersGenerator    *LoadBalancerServiceGenerator
	}

	Objects []runtime.Object

	PodSelector map[string]string

	Option        interface{}
	LoggingOption struct {
		LogPrefix string
	}
)

const (
	LogPrefixAnnotation                       = "platform-v.drop-app/log-prefix"
	FullNameAnnotation                        = "platform-v.drop-app/fullname"
	ResourceBelongsToAnnotation               = "platform-v.drop-app/belongs-to"
	ResourceBelongsToAnnotationValueDelimiter = ","

	projectLabel = "platform-v.drop-app/project"
)

func getOption[O Option](options ...Option) (O, bool) {
	var or O
	for _, opt := range options {
		o, ok := opt.(O)
		if ok {
			return o, true
		}

	}

	return or, false
}

func NewGenerator(properties configuration.GeneratorProperties) *Generator {
	loadBalancers := NewLoadBalancerServiceGenerator()
	services := NewServiceGenerator()

	return &Generator{
		ResourceQuotaGenerator:    NewResourceQuotaGenerator(properties),
		LimitRangeGenerator:       NewLimitRangeGenerator(properties),
		VolumeClaimsGenerator:     NewVolumeClaimsGenerator(properties),
		ImagePullSecretsGenerator: NewImagePullSecretsGenerator(properties),
		DeploymentsGenerator:      NewDeploymentsGenerator(properties, loadBalancers, services),
		ServicesGenerator:         services,
		LoadBalancersGenerator:    loadBalancers,
	}
}

func (g *Generator) GenerateSuspended(namespace internal.Namespace, compose model.Compose, opts ...Option) ([]runtime.Object, error) {
	return g.generate(namespace, compose, true, opts...)
}

func (g *Generator) Generate(namespace internal.Namespace, compose model.Compose, opts ...Option) ([]runtime.Object, error) {
	return g.generate(namespace, compose, false, opts...)
}

func (g *Generator) generate(namespace internal.Namespace, compose model.Compose, suspended bool, opts ...Option) ([]runtime.Object, error) {
	if compose.Version == "" {
		return nil, fmt.Errorf("compose version is absent for namespace '%s'", namespace)
	}

	if len(compose.Services) == 0 {
		return nil, fmt.Errorf("empty services specified in compose for namespace '%s'", namespace)
	}

	resources := make(Objects, 0)

	resources = appendObjects(resources, g.ResourceQuotaGenerator.Generate())
	resources = appendObjects(resources, g.LimitRangeGenerator.Generate())
	resources = appendObjects(resources, generateNamespaces(namespace))
	resources = appendObjects(resources, generateNetworkPolicies(namespace))

	volumeClaims := g.VolumeClaimsGenerator.Generate(compose.Volumes)
	resources = appendObjects(resources, volumeClaims)

	imagePullSecrets := g.ImagePullSecretsGenerator.Generate()
	resources = appendObjects(resources, imagePullSecrets)

	loadBalancers, err := g.LoadBalancersGenerator.Generate(compose.Services)
	if err != nil {
		return nil, fmt.Errorf("failed to generate loadbalancers: %w", err)
	}
	resources = appendObjects(resources, loadBalancers)

	deployments, err := g.DeploymentsGenerator.Generate(compose.Services, suspended, volumeClaims, imagePullSecrets, opts...)
	if err != nil {
		return nil, fmt.Errorf("failed to generate deployments: %w", err)
	}
	resources = appendObjects(resources, deployments)

	services, err := g.ServicesGenerator.Generate(compose.Services)
	if err != nil {
		return nil, fmt.Errorf("failed to generate services: %w", err)
	}
	resources = appendObjects(resources, services)

	result := make([]runtime.Object, 0, len(resources))

	for _, resource := range resources {
		current := resource

		if current != nil {
			result = append(result, current)
		}
	}

	return result, nil
}

func appendObjects[T runtime.Object](left Objects, right []T) Objects {
	result := make([]runtime.Object, 0, len(left)+len(right))

	for _, item := range left {
		current := item
		result = append(result, current)
	}

	for _, item := range right {
		current := item
		result = append(result, current)
	}

	return result
}

func appendPodSelectors(selectors ...PodSelector) (PodSelector, error) {
	result := PodSelector{}

	for _, selector := range selectors {
		for key, value := range selector {
			if existing, ok := result[key]; ok && existing != value {
				return nil, fmt.Errorf("selector '%s':'%s' already exists with different value '%s'", key, value, existing)
			}

			result[key] = value
		}
	}

	return result, nil
}

// Several kubernetes resources, with zero length slice in property,
// after serialization + deserialization process,
// have such properties equals to nil
//
// so, to be able to compare such objects, we're repeating such logic
//
// Examples:
// - Deployment.Spec.Containers.Ports
// - Deployment.Spec.Volumes
func sliceOrNil[T any](capacity int) []T {
	if capacity == 0 {
		return nil
	}

	return make([]T, 0, capacity)
}
